Skip to content

[DSL] Add extra commands/abilities to DSL scripts/rules#5481

Draft
Nadahar wants to merge 8 commits intoopenhab:mainfrom
Nadahar:dsl-extras
Draft

[DSL] Add extra commands/abilities to DSL scripts/rules#5481
Nadahar wants to merge 8 commits intoopenhab:mainfrom
Nadahar:dsl-extras

Conversation

@Nadahar
Copy link
Copy Markdown
Contributor

@Nadahar Nadahar commented Apr 9, 2026

Add access to various system registries, OSGi instances and the ability to run and enable/disable rules to DSL scripts.

This has been considered "impossible" to do, yet I've met no special obstacles, and it seems to work with my (limited) testing. I'm therefore skeptical that there might be something that I've missed, like additional startup issues caused by ScriptServiceUtil referencing additional instances, thus depending on them. But, I've observed no such problems.

Additional context/information about the historical problems with this is appreciated, but as far as I can see at the moment, this works without complications.

@Nadahar Nadahar requested a review from a team as a code owner April 9, 2026 16:04
@Nadahar
Copy link
Copy Markdown
Contributor Author

Nadahar commented Apr 9, 2026

Also, consider the specific new commands I've implemented as more of a suggestion. This can be tweaked as desired.

@rkoshak Perhaps you have something to add here?

@Nadahar
Copy link
Copy Markdown
Contributor Author

Nadahar commented Apr 9, 2026

The itest fails because of some unresolved reference. I'm trying to figure it out, but if anybody has any hints of how to figure out the problem, it would be appreciated, because I frankly have no idea how to handle the bndrun resolution.

@Nadahar
Copy link
Copy Markdown
Contributor Author

Nadahar commented Apr 9, 2026

I don't think the itest fails over dependencies, but that the startlevel is insufficient:

Components implementing org.openhab.core.model.script.ScriptServiceUtil:
129 [UNSATISFIED_REFERENCE] org.openhab.core.model.script.ScriptServiceUtil in org.openhab.core.model.script
	$006 (org.openhab.core.automation.RuleManager)
		54 [UNSATISFIED_REFERENCE] org.openhab.core.automation.internal.RuleEngineImpl in org.openhab.core.automation
			$004 (org.openhab.core.service.StartLevelService)

If I understand it correctly, RuleEngineImpl can't start if the startlevel is below 40 - and in the test, it is. This requirement now applies to ScriptServiceUtil because I've included a reference to RuleManager (which is implemented by RuleEngineImpl) in the constructor.

So, the question is: Is it actually a problem that ScriptServiceUtil can't start before startlevel 40, or does the test need to be modified to get around this?

Ravi Nadahar added 2 commits April 9, 2026 20:14
…ty to run and enable/disable rules to DSL scripts

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>
Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>
@Nadahar
Copy link
Copy Markdown
Contributor Author

Nadahar commented Apr 9, 2026

I've set the startlevel in the test to 40, which is required for the script engine to start, and it seems to resolve the problem with the test. I'm still uncertain as to whether reaching startlevel 40 before the ScriptServiceUtil can start is a problem or not.

Are there scripts being executed before startlevel 40 is reached?

@Nadahar
Copy link
Copy Markdown
Contributor Author

Nadahar commented Apr 9, 2026

Now there's a new test failure, not sure exactly how it is related yet:

TEST org.openhab.core.model.rule.runtime.DSLRuleProviderTest#testSimpleRules() <<< ERROR: Cannot invoke "org.eclipse.emf.ecore.resource.Resource.load(java.io.InputStream, java.util.Map)" because "resource" is null
java.lang.NullPointerException: Cannot invoke "org.eclipse.emf.ecore.resource.Resource.load(java.io.InputStream, java.util.Map)" because "resource" is null

Copy link
Copy Markdown

@rkoshak rkoshak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments.

* @throws IllegalArgumentException If a rule with the specified UID doesn't exist.
*/
@ActionDoc(text = "run the rule with the specified UID, condition evaluation setting and context")
public static Map<String, Object> runRule(String ruleUID, boolean considerConditions, Map<String, Object> context) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems weird to make considerConditions the second argument given there is a version of the method that includes the context but doesn't take the condition boolean. That is how it appears in JS. I don't know the other languages.

As an end user who knows nothing of the RuleManager, I would expect the actions to be:

  • runRule(ruleUID)
  • runRule(ruleUID, context)
  • runRule(ruleUID, context, considerConditions)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I followed the parameter order of the underlying methods. I'd say that it's not a big deal either way, but that perhaps it would be useful with a runRule(ruleUID, considerConditions) as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When it comes to JS, that's an entirely different thing. It allows that you only specify some parameters, the rest are undefined (?). This means that you must try to guesstimate what parameters people are most likely to drop, and then list those last.

Here, there is no such thing, you must have an exact signature match, so the order doesn't have any significance, unless you use varargs that can't have anything following them (in a sense, in JS, "everything" are varargs of Object). Because collections can often optionally be specified as varargs, you often put them last - even when a varargs method doesn't exist. I could have made a version here where context could be Entry<String, Object>.... In that case, context would have to be last, and my guess is that because that's a thing people get used to, it is somehow intuitive to put the collection last anyway. I didn't do this here because it's awkward to have to create Entry instances for each "context pair" you wish to include, so you might as well provide a Map.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we really wanted to go down the road of type-unsafety, I could also let the last parameter be Object..., and then "expect" them to come in pairs where the first was a string and the second some object. The method could then "build" the map and throw an exception if the parameters didn't meet expectations. It might be the most convenient way one could supply context, since you could just supply strings directly in the call, but it "feels" hacky and something you'd do in JS or similar, not in a typed language.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but that perhaps it would be useful with a runRule(ruleUID, considerConditions) as well?

I'm certainly not against it.

so the order doesn't have any significance,

I just brought up the JS as an example of following a pattern. Given runRule(ruleUID) and runRule(ruleUID, context), the expected next method signature would be runRule(ruleUID, context, considerConditions). Each new signature adds a new argument to the end of the list of arguments. Placing the considerConditions as the second argument violates that expectation.

It's not about technical stuff. It's about making decisions that allow users who are not as familiar with the source code to correctly guess what is correct.

Of course, if you add the newly proposed method there is no more pattern and the order doesn't matter as much.

* @author Ravi Nadahar - Initial contribution
*/
@NonNullByDefault
public class System {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"System" is a pretty generic and almost meaningless name. Would something like "Registries" be feasible? Everything here either exposes or lets users interact with one of the registries.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name can be anything, but it does handle more than registries. It also provides OSGi instances and some rule manipulation methods. I chose system because I couldn't really find anything "suitable".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also considered names like "OSGi", "Core", "OpenHAB", but they can all be considered "wrong" or misunderstood. The users don't actually have to see this name though. Look at ScriptExecution that creates timers for example. My guess is that nobody knows/cares, because they don't have to qualify the method with the class name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about "Services" or "Utility"? They don't mean much more than "System", but it's hard to find something that means much and also fits the methods.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about "Helper"?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look at ScriptExecution that creates timers for example. My guess is that nobody knows/cares, because they don't have to qualify the method with the class name.

All of the JSR223 languages have to deal with those class names. It's really only Rules DSL that doesn't. But since this is all for the benefit of Rules DSL in the first place maybe it doesn't matter.

that means much and also fits the methods.

Which brings up something I decided not to mention. Would it make more sense to split this into separate classes, one for rules, one for metadata, one for things and one for Items? Then the naming becomes obvious and clear.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That could be done, but I'm not sure if there's much to gain by doing it. It would mean more classes that had to be added to the "implicit DSL context", but whether that has any practical consequences I don't know. Probably not.

* @return The {@link MetadataRegistry}.
*/
@ActionDoc(text = "get the metadata registry")
public static MetadataRegistry getMetadataRegistry() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even once you gain access to the MetadataRegistry, creating and adding or modifying is a pain because it requires creating a bunch of custom classes. Maybe not for this PR but eventually adding some helper methods like are here for rules would be beneficial.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could add more "helper tools" if somebody just give me a hint of what to make (a reference to something else that does something similar)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an example, JS provides a metadata on the class it uses to expose the ItemRegistry which lets one access an Items metadata. See https://openhab.github.io/openhab-js/items.metadata.html.

In addition it provides access to those methods on the Item itself. The methods include:

  • addMetadata(itemOrName, namespace, value, configurationopt, persistopt)
  • getMetadata(itemOrName, namespaceopt)
  • removeMetadata(itemOrName, namespaceopt)
  • replaceMetadata(itemOrName, namespace, value, configurationopt)

itemOrName can be an instance of an Item or the name of an Item. value is a String; empty String is allowed and I think null becomes the empty String. namespace is a String of course. configurationopt is an optional Object though I'd expect it to be a Map in Rules DSL.

persistopt is a new field I'm not familiar with.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those methods are quite similar to those I've already created. I could certainly add variants that accepts Item instead of "itemName", if that's easier to provide (if you already have the Item, I guess it's easy to just pass that on).

When in comes to persistopt, that is related to "stuff created by a JS script" and not really relevant here. There's a separate metadata provider that doesn't persist, so that the metadata only exists until the file is unloaded. So, I'm guessing that this option determines whether to store them there or in the "managed metadata provider".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unfortunate that they have called one method replaceMetadata when the underlying method is called updateMetadata. They are the exact same thing, but I prefer to stick to what's underneath, so that it's easier for people to see how it all ties together. I went with "update" instead of "replace".

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

. I could certainly add variants that accepts Item instead of "itemName"

I think that is not as useful in DSL as it is in JS.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've already done that, but it's done as "extensions", so it becomes:

  • item.addMetadata(namespace, value [, context etc.])
  • item.getMetadata(namespace)
  • etc.

Given that Items are magically available in DSL, this should make it very easy to manipulate Item metadata.

@Nadahar
Copy link
Copy Markdown
Contributor Author

Nadahar commented Apr 9, 2026

The current test failure seems to have some complicated timing reason. The following line in ModelRepository.validateModel() returns null, which doesn't seem expected:

        Resource resource = resourceSet.createResource(URI.createURI(PREFIX_TMP_MODEL + name));

This results in an NPE. The question is why it returns null now all of a sudden, and it probably has to do with how the changes in this PR has impacted the startup order of OSGi components. The Javadocs for createResource() says:

Creates a new resource, of the appropriate type, and returns it.

It delegates to the resource factory registry to determine the correct factory, and then it uses that factory to create the resource and adds it to the contents. If there is no registered factory, null will be returned; when running within Eclipse, a default XMI factory will be registered, and this will never return null.

I'm not sure what this mythical factory is and why it suddenly can't be found, but I assume that it has something to do with ScriptServiceUtil now having to wait for startlevel 40 before starting. The details of how this all relates aren't clear to me though.

Ravi Nadahar added 2 commits April 10, 2026 03:34
Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>
This reverts commit ee8ae1e.

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>
@Nadahar
Copy link
Copy Markdown
Contributor Author

Nadahar commented Apr 10, 2026

I changed the strategy regarding RuleManager. It requires startlevel 40 to start, which caused all kind of timing-based issues with tests when it became a reference of ScriptServiceUtil. I've thus modified it to be a dynamic reference, so that ScriptServiceUtil can start without RuleManager having been started.

This has resolved the test problems, but comes with the caveat that getRuleManager() might return null. Still, all in all, I think it's the best solution. It shouldn't return null when rules are actually running, since it's the one executing rule actions. So, in reality, it might actually never return null.

@Nadahar
Copy link
Copy Markdown
Contributor Author

Nadahar commented Apr 10, 2026

In case somebody wants to test it for themselves, this bundle should be all that's needed.

org.openhab.core.model.script-5.2.0-SNAPSHOT.jar.txt

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>
@Nadahar
Copy link
Copy Markdown
Contributor Author

Nadahar commented Apr 10, 2026

@rkoshak I've added some additional methods for manipulation of metadata. Is that somewhat what you had in mind? I've also reluctantly implemented the varargs "solution" for the maps, so that it's possible to simply call e.g. addMetadata(namespace, itemName, value, configKey1, configValue1, configKey2, configValue2...) (the same for the runNow() context).

Since these are just suggestions, I haven't bothered to finish the Javadocs, as I'm sure they'll change, which makes the time I spend writing the documentation a waste.

@Nadahar Nadahar marked this pull request as draft April 10, 2026 13:54
Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>
@Nadahar
Copy link
Copy Markdown
Contributor Author

Nadahar commented Apr 10, 2026

@rkoshak I've added even more overloads, so not all the metadata methods accept either item name or Item as the first argument. Doesn't that make it pretty close to what JS provides?

@Nadahar
Copy link
Copy Markdown
Contributor Author

Nadahar commented Apr 10, 2026

@lolodomo There's no a lot of activity here, maybe you're interested in this?

This isn't important for me in any way, but seen in light of all the other discussions about DSL versus other scripting languages and the difference in what you can do, I thought that this might be a welcome contribution to close the gap.

@Nadahar
Copy link
Copy Markdown
Contributor Author

Nadahar commented Apr 10, 2026

Ï've never fully understood the "action" concept in OH. I understand that they are similar to functions/methods, but not what the significance of making things "actions" is. We have for example org.openhab.core.model.script.actions.Things, which doesn't implement ActionService, but exists in package actions. Then, we have org.openhab.core.model.script.internal.engine.action.ThingActionService, which does implement ActionService. The former basically delegates to the latter, so they both provide the exact same functionality.

Why this structure? Is an ActionService available more broadly, to other scripting languages for example? It doesn't seem so, since it's placed in an internal package in the code that makes up DSL.

Ravi Nadahar added 2 commits April 11, 2026 03:57
Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>
Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>
@Nadahar
Copy link
Copy Markdown
Contributor Author

Nadahar commented Apr 11, 2026

I've reorganized System into different classes. I've also discovered and implemented extensions, so that it's now possible to do e.g.:

var shared = getRule("shared-test-1")
logInfo("test", shared.getUID() + " enabled: " + shared.isEnabled())
shared.setEnabled(true)
logInfo("test", shared.getUID() + " enabled: " + shared.isEnabled())
shared.run()

The "full versions" are also there, so you can call setRuleEnabled(ruleUID, enabled) instead. I've done the same for Items, so you can just do e.g. someItem.addMetadata(namespace, value).

@openhab-bot
Copy link
Copy Markdown
Collaborator

This pull request has been mentioned on openHAB Community. There might be relevant details there:

https://community.openhab.org/t/rules-and-rule-templates-yaml-integration/168568/222

@dilyanpalauzov
Copy link
Copy Markdown
Contributor

#5484 adds Import-Package: org.openhab.core.automation to both org.openhab.core.model.rule/bnd.bnd and org.openhab.core.model.script/bnd.bnd. In turn org.openhab.core.automation.RuleManager can be resolved in both DSL Scripts and Textual DSL Rules.

Running afterwards RuleManager.runNow() is demonstrated at openhab/openhab-docs#2701.

@Nadahar
Copy link
Copy Markdown
Contributor Author

Nadahar commented Apr 13, 2026

#5484 adds Import-Package: org.openhab.core.automation to both org.openhab.core.model.rule/bnd.bnd and org.openhab.core.model.script/bnd.bnd. In turn org.openhab.core.automation.RuleManager can be resolved in both DSL Scripts and Textual DSL Rules.

Running afterwards RuleManager.runNow() is demonstrated at openhab/openhab-docs#2701.

Adding org.openhab.core.automation import to model.rule I'm sure is useful, I'm surprised it's not already there. This PR is about the model.script bundle, where it also adds the same import.

That said, the goal of this PR isn't to make it possible to do those things, but to make it reasonably convenient. As long as imports can be resolved, you can always use FrameworkUtil manually like your example does, but I wouldn't call that very user-friendly.

This PR has getInstance() to do exactly the same operation for you, without having to deal with all the "OSGi details":

    public static @Nullable <T> T getInstance(Class<T> clazz) {
        Bundle bundle = FrameworkUtil.getBundle(clazz);
        if (bundle != null) {
            BundleContext bc = bundle.getBundleContext();
            if (bc != null) {
                ServiceReference<T> ref = bc.getServiceReference(clazz);
                if (ref != null) {
                    return bc.getService(ref);
                }
            }
        }
        return null;
    }

So, I don't see these two as "competing" in any way.

@dilyanpalauzov
Copy link
Copy Markdown
Contributor

After a = BundleContext.getService(…) there must be corresponding BundleContext.ungetService(a) - https://docs.osgi.org/javadoc/osgi.core/8.0.0/org/osgi/framework/BundleContext.html#getService-org.osgi.framework.ServiceReference- - otherwise resources stay allocated unnecessary.

By using the function getInstance() from the previous comment it is not possible to invoke ungetService() on the result of getService().

@dilyanpalauzov
Copy link
Copy Markdown
Contributor

After a = BundleContext.getService(…) there must be corresponding BundleContext.ungetService(a)

Rectification:

After a = BundleContext.getServiceReference(…) there must be corresponding BundleContext.ungetService(a).

@Nadahar
Copy link
Copy Markdown
Contributor Author

Nadahar commented Apr 14, 2026

I didn't remember that the services were reference counted, so I'll have to make some modifications to accommodate that. But, as I see it, we can't expect script authors to use try/finally to make sure to release the reference. I've looked a bit in core, and it seems like this is "mishandled" other places as well. If you look at JS Scripting, it does the exact same thing:

https://openhab.github.io/openhab-js/osgi.js.html#line38

Since it's unrealistic that script authors can manage the reference counting, I think the best thing to do is to release the reference before returning it. That will work just fine as long as something else keeps a "counted reference" to the instance. In OH, this will almost always be the case, there are other components that reference "everything".

As far as I understand, OSGi uses this reference counting to automatically stop services that aren't used by anything. The fact that a service is referenced doesn't prevent an explicit shutdown, so it's not like "a lock", it just prevents automatic shutdown. Since the script acquiring the reference should never have the only reference, releasing it before returning should work fine in practice. In the very unlikely scenario that this isn't the case, the script will fail when it tries to use the service if OSGi has shut it down in the meanwhile. I think that is acceptable, given that it is extremely unlikely to happen, and it should be better than to "leak references".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants